/*
 * Copyright (c) 2009-2011 Dropbox, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */


package com.dropbox.client2.exception;

import java.util.Map;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;

/**
 * Wraps any non-200 HTTP responses from an API call. See the constants in this
 * class for the meaning of each error code. You'll typically only want to
 * handle a few specific error codes and show some kind of generic error or
 * retry for the rest.
 */
public class DropboxServerException extends DropboxException {

    public static class Error {

        /** English version of the error. */
        public String error;

        /** The error in the user's locale, if intended to be displayed to the user. */
        public String userError;

        @SuppressWarnings("unchecked")
        public Error(Map<String, Object> map) {
            if (map != null) {
                Object err = map.get("error");
                if (err instanceof String) {
                    error = (String)err;
                } else if (err instanceof Map<?, ?>) {
                    Map<String, Object> detail = (Map<String, Object>)err;
                    for (Object val: detail.values()) {
                        if (val instanceof String) {
                            error = (String)val;
                        }
                    }
                }
                Object uerr = map.get("user_error");
                if (uerr instanceof String) {
                    userError = (String)uerr;
                }
            }
        }
    }

    /** The request was successful. This won't ever be thrown in an exception. */
    public static final int _200_OK = 200;

    /** Moved to a new location temporarily. */
    public static final int _302_FOUND = 302;

    /**
     * Contents have not changed (from the given hash, revision, ETag, or similar parameter).
     */
    public static final int _304_NOT_MODIFIED = 304;

    /** Bad input parameter. Error message should indicate which one and why. */
    public static final int _400_BAD_REQUEST = 400;

    /** Bad or expired access token. Need to re-authenticate user. */
    public static final int _401_UNAUTHORIZED = 401;

    /** Usually from an invalid app key pair or other permanent error. */
    public static final int _403_FORBIDDEN = 403;

    /** Path not found. */
    public static final int _404_NOT_FOUND = 404;

    /**
     * Request method not allowed. You shouldn't see this unless writing your
     * own API calls.
     */
    public static final int _405_METHOD_NOT_ALLOWED = 405;

    /** Too many metadata entries to return. */
    public static final int _406_NOT_ACCEPTABLE = 406;

    public static final int _409_CONFLICT = 409;

    /**
     * Typically from trying to upload over HTTP using chunked encoding. The
     * Dropbox API currently does not support chunked transfer encoding.
     */
    public static final int _411_LENGTH_REQUIRED = 411;

    /** When a thumbnail cannot be created for the input file. */
    public static final int _415_UNSUPPORTED_MEDIA = 415;

    /**
     * Internal server error. Best to try again or wait until corrected by
     * Dropbox.
     */
    public static final int _500_INTERNAL_SERVER_ERROR = 500;

    /** Not implemented. */
    public static final int _501_NOT_IMPLEMENTED = 501;

    /** If a Dropbox server is down - try again later. */
    public static final int _502_BAD_GATEWAY = 502;

    /** If a Dropbox server is not working properly - try again later. */
    public static final int _503_SERVICE_UNAVAILABLE = 503;

    /** User is over quota. */
    public static final int _507_INSUFFICIENT_STORAGE = 507;

    private static final long serialVersionUID = 1L;

    /** The body, if any, of the returned error. */
    public Error body;

    /** The HTTP error code. */
    public int error;

    /** The reason string associated with the error. */
    public String reason;

    /** The server string from the headers. */
    public String server;

    /** The location string from the headers (to handle redirects). */
    public String location;

    /**
     * Creates a {@link DropboxServerException} from an {@link HttpResponse}.
     */
    public DropboxServerException(HttpResponse response) {
        this.fillInStackTrace();
        StatusLine status = response.getStatusLine();
        error = status.getStatusCode();
        reason = status.getReasonPhrase();
        server = getHeader(response, "server");
        location = getHeader(response, "location");
    }

    /**
     * Creates a {@link DropboxServerException} from an {@link HttpResponse}.
     * The rest parameter must be a Map of String to Object.
     */
    @SuppressWarnings("unchecked")
    public DropboxServerException(HttpResponse response, Object rest) {
        this(response);

        if (rest != null && rest instanceof Map<?, ?>) {
            body = new Error((Map<String, Object>)rest);
        }
    }

    /**
     * When this exception comes from creating a new account, returns whether
     * the request failed because an account with the email address already
     * exists.
     */
    public boolean isDuplicateAccount() {
        return (error == 400 && body != null && body.error.contains("taken"));
    }

    @Override
    public String toString() {
        return "DropboxServerException (" + server + "): " + error + " " + reason + " (" + body.error + ")";
    }

    /**
     * Whether the given response is valid when it has no body (only some error
     * codes are allowed without a reason, currently 302 and 304).
     */
    public static boolean isValidWithNullBody(HttpResponse response) {
        int code = response.getStatusLine().getStatusCode();
        if (code == _302_FOUND) {
            String location = getHeader(response, "location");

            if (location != null) {
                int loc = location.indexOf("://");
                if (loc > -1) {
                    location = location.substring(loc+3);
                    loc = location.indexOf("/");
                    if (loc > -1) {
                        location = location.substring(0, loc);
                        if (location.toLowerCase().contains("dropbox.com")) {
                            return true;
                        }
                    }
                }
            }
        } else if (code == _304_NOT_MODIFIED) {
            return true;
        }
        return false;
    }

    private static String getHeader(HttpResponse response, String name) {
        String value = null;
        Header serverheader = response.getFirstHeader(name);
        if (serverheader != null) {
            value = serverheader.getValue();
        }
        return value;
    }
}
